package com.nulldev.util.web.YouTubeExtractor.extractors.next;

import java.net.URL;
import java.time.Duration;
import java.util.ArrayDeque;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import com.nulldev.util.JVM.JVM;
import com.nulldev.util.VariableAPI.ArrayUtils;
import com.nulldev.util.VariableAPI.MathUtil;
import com.nulldev.util.VariableAPI.MathUtil.TimeUtil;
import com.nulldev.util.VariableAPI.StringsUtil;
import com.nulldev.util.concurrency.threadIt.v4.emapi.ExecutorManager;
import com.nulldev.util.concurrency.threadIt.v4.emapi.IExecutorManager;
import com.nulldev.util.concurrency.threadIt.v4.emapi.uft.Executable;
import com.nulldev.util.data.variable;
import com.nulldev.util.data.Arrays.arrays.FastLists;
import com.nulldev.util.data.Date.Dates;
import com.nulldev.util.languages.json.uniapi.UniJsonIterableValue;
import com.nulldev.util.languages.json.uniapi.UniJsonValue;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.manage.MarkedForRemoval;
import com.nulldev.util.web.HttpClient.HttpClient;
import com.nulldev.util.web.HttpClient.HttpResponse;
import com.nulldev.util.web.URLUtils.Entry;
import com.nulldev.util.web.URLUtils.URLUtils;
import com.nulldev.util.web.URLUtils.fuckTracking.FuckTracking;
import com.nulldev.util.web.YouTubeExtractor.YouTubeExtractor;
import com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants;
import com.nulldev.util.web.YouTubeExtractor.YoutubeExtractionOptions;
import com.nulldev.util.web.YouTubeExtractor.comments.YTComments.YTCommentInitData;
import com.nulldev.util.web.YouTubeExtractor.extractors.IYouTubeExtractor;
import com.nulldev.util.web.YouTubeExtractor.extractors.IYouTubeExtractorConstants;
import com.nulldev.util.web.YouTubeExtractor.extractors.util.SharedExtractorUtil;
import com.nulldev.util.web.YouTubeExtractor.formats.AudioCodec;
import com.nulldev.util.web.YouTubeExtractor.formats.FileType;
import com.nulldev.util.web.YouTubeExtractor.formats.Format;
import com.nulldev.util.web.YouTubeExtractor.formats.Formats;
import com.nulldev.util.web.YouTubeExtractor.formats.VideoCodec;
import com.nulldev.util.web.YouTubeExtractor.metadata.YouTubeMetadata;
import com.nulldev.util.web.YouTubeExtractor.playlist.Playlist;
import com.nulldev.util.web.YouTubeExtractor.playlist.Playlist.PlaylistEntry;
import com.nulldev.util.web.YouTubeExtractor.signatures.SignatureUpdates;
import com.nulldev.util.web.YouTubeExtractor.signatures.extractor.Extractor;
import com.nulldev.util.web.YouTubeExtractor.subtitles.YouTubeCaptions;
import com.nulldev.util.web.YouTubeExtractor.video.ExtractedVideo;
import com.nulldev.util.web.YouTubeExtractor.video.ExtractionResult;
import com.nulldev.util.web.YouTubeExtractor.video.YouTubeVideo;
import com.nulldev.util.web.YouTubeExtractor.video.YouTubeVideo.FormatType;
import com.nulldev.util.web.legacyURI.LegacyURIUtils;

@SuppressWarnings("deprecation")
@MarkedForRemoval(reason = "NFEE is heavily threaded already, no purpose in keeping this extractor", plannedRemovalWhen = "2.3")
public class YouTubeNextFEEMT extends IYouTubeExtractor {

	private static final IExecutorManager td = ExecutorManager.get();
	public static boolean USE_OLD_FMT_METHOD = false;

	/**
	 * YouTubeExtractor 2.1: NextFastExecutionExecutor-MultiThreaded
	 * 
	 * Fixes: <br>
	 * - ALL signature decipherer issues. <br>
	 * Improvements: <br>
	 * - Faster. <br>
	 * - Much less code. <br>
	 * TODO: <br>
	 * - JSON_SHORTCUT_SUPPORT
	 */

	private static final Logger logger = LoggerFactory.getLoggerD(
			// Arguments.isArg("--YouTubeExtractor.debug") ||
			// Arguments.isArg("--YouTubeExtractor.NextFEE.debug"),
			true, true);
	public static final String VERSION = "1.0.1-7";

	private static String generateURL(final String videoID, final Map<String, Object> arguments) {
		final StringBuilder b = new StringBuilder("https://www.youtube.com/watch?v=");
		b.append(videoID);
		if (arguments.containsKey(MAP_TYPE_FORCE_ENGLISH_LANG_COMPAT))
			b.append("&hl=EN");
		if (arguments.containsKey(MAP_TYPE_ENABLE_CONTROVERSY_BYPASS)) {
			b.append("&bpctr=");
			b.append(System.currentTimeMillis());
		}
		if (arguments.containsKey(MAP_TYPE_ENABLE_VERIFIED_SPOOFING)) {
			b.append("&has_verified=1");
		}
		return b.toString();
	}

	@Override
	@Deprecated
	public Executable<ExtractedVideo> generateTask(String videoID, YoutubeExtractionOptions... options) {
		logger.warn("generateTask(String, YoutubeExtractionOptions) is deprecated and marked for removal.");
		return generateTask(videoID, null, options);
	}

	@Override
	@Deprecated
	public Executable<ExtractedVideo> generateTask(String videoID, String extraArgs, YoutubeExtractionOptions... options) {
		logger.warn("generateTask(String, String, YoutubeExtractionOptions) is deprecated and marked for removal.");
		return generateTask(IYouTubeExtractorConstants.convertFromLegacy(videoID, extraArgs, options));
	}

	@Override
	public Executable<ExtractedVideo> generateTask(final Map<String, Object> arguments) {
		if (!isValidMap(arguments))
			throw new IllegalArgumentException("Invalid map: " + arguments);
		final String videoID = (String) arguments.get(MAP_TYPE_VIDEO_ID);
		if (!isValidVideoID(videoID))
			throw new IllegalArgumentException("Invalid video ID: " + videoID);
		final StringBuilder extras = new StringBuilder();
		if (arguments.containsKey(MAP_TYPE_PLAYLIST_STR)) {
			extras.append("&list=");
			extras.append(arguments.get(MAP_TYPE_PLAYLIST_STR));
		}
		final variable<Integer> _index = new variable<Integer>(-1);
		if (arguments.containsKey(MAP_TYPE_PLAYLIST_INDEX)) {
			extras.append("&index=");
			final int ix = (int) arguments.get(MAP_TYPE_PLAYLIST_INDEX);
			extras.append(ix);
			_index.set((int) arguments.get(MAP_TYPE_PLAYLIST_INDEX));
		}
		final String extraArgs = extras.toString();
		final Executable<ExtractedVideo> task = td.makeTask(() -> {
			final List<YouTubeVideo> results = FastLists.list(OUTPUT_LIST_DEFAULT_SIZE);
			logger.debug("=> Generating URL...");
			// int index = -1;
			logger.debug("ea: " + extraArgs);
			final URL url = URLUtils.parseURL(generateURL(videoID, arguments) + extras);
			logger.debug("URL: " + generateURL(videoID, arguments) + extras);
			Entry[] _args = (!isEnabled(arguments, MAP_TYPE_DISABLE_CONSENT_COOKIE_COMPAT)) ? YouTubeExtractor.DEFAULT_COOKIES : null;
			if (isEnabled(arguments, MAP_TYPE_SEND_FAKE_COOKIES_COMPAT) || isEnabled(arguments, MAP_TYPE_ENFORCE_PRIVACY_MODE_COMPAT)) {
				if (_args == null)
					_args = new Entry[0];
				_args = ArrayUtils.add(_args, new Entry("Cookie", StringsUtil.randomString(64)));
			}
			if (isEnabled(arguments, MAP_TYPE_ENFORCE_PRIVACY_MODE_COMPAT) || isEnabled(arguments, MAP_TYPE_EMULATE_INSECURE_PROXY_COMPAT)) {
				if (_args == null)
					_args = new Entry[0];
				_args = ArrayUtils.addAll(_args, FuckTracking.protect());
			}
			final Entry[] __args = _args;
			final variable<String> cookiePayload = new variable<String>("");
			final Executable<Object> _result = td.submit(() -> {
				try {
					logger.debug("=> Downloading metadata...");
					final HttpResponse<String> x = HttpClient.getString().url(url).timeout(Duration.ofSeconds(10)).headers(__args).create();
					String _cookiePayload = "";
					for (final String cookie : x.cookies()) {
						_cookiePayload += cookie.split(";")[0] + ";";
					}
					if (_cookiePayload.endsWith(";")) {
						_cookiePayload = _cookiePayload.substring(0, _cookiePayload.length() - 1);
					}
					cookiePayload.set(_cookiePayload);
					return x.dataThenClose();
				} catch (Exception e) {
					JVM.errorln(e);
					return new ExtractedVideo(null, null, ExtractionResult.FAILED).setMessage("FAILED_STAGE_UNABLE_TO_GET_METADATA");
				}
			});
			td.queueTask(_result);
			if (_result.waitTillCompletion() instanceof ExtractedVideo) {
				return (ExtractedVideo) _result.result();
			}
			final String result = (String) _result.result();
			logger.debug("=> Parsing metadata...");
			logger.debug("=> Accquiring decipherer.");
			final Executable<Extractor> e = td.submit(() -> {
				try {
					final String playerID = SignatureUpdates.getPlayerURL(result);
					final String variantsChecksum = YouTubeExtractorConstants.ENABLE_VARIANTS_CHECKSUM ? SignatureUpdates.getVariantsChecksum(result) : null;
					if (SignatureUpdates.requiresUpdate(playerID, variantsChecksum)) {
						logger.warn("Data mismatch, Reaccquiring decipherer!");
						return YouTubeExtractor.extractor.reaccquire();
					}
					return YouTubeExtractor.extractor.getAuto();
				} catch (Exception ex) {
					throw new RuntimeException(ex);
				}
			});
			final Executable<UniJsonValue> ytInitialData = td.submit(() -> {
				try {
					final String ss = StringsUtil.substringBetweenO(result, "window[\"ytInitialData\"] = {", "};");
					if (ss == null) {
						final String ss1 = StringsUtil.substringBetweenO(result, ">var ytInitialData = {", "};");
						if (ss1 == null) {
							logger.error("Failed to fetch ytInitialData!");
							logger.debug(result);
							return null;
						}
						return UniJsonValue.parse("{" + ss1 + "}", true);
					}
					return UniJsonValue.parse("{" + ss + "}", true);
				} catch (Exception ex) {
					logger.error("Failed to parse ytInitialData!", ex);
					return null;
				}
			});
			td.queueTask(e);
			final String raw_json0 = (String) td.submit(() -> {
				final String f = StringsUtil.substringBetweenO(result, "ytplayer.config = ", ";ytplayer.load");
				if (f == null || f.isEmpty())
					return null;
				return "{\"" + f;
			}).waitTillCompletion();
			String raw_json = null;
			if (raw_json0 == null) {
				final String rn = StringsUtil.substringBetweenO(result, ">var ytInitialPlayerResponse = ", "};");
				if (rn == null) {
					logger.warn("Failed to correct raw_JSON payload!");
					logger.debug(_result);
				} else {
					raw_json = rn + "}";
					logger.trace("New Payload: " + raw_json);
					logger.warn("Corrected raw_JSON payload!");
				}
			} else
				raw_json = raw_json0.substring(2, raw_json0.length());
			if (result.contains("verify_controversy")) {
				logger.warn("YouTubeNextFEEMT -> This is a 'controversial' video, retrying with bypass...'");
				arguments.putIfAbsent(MAP_TYPE_ENABLE_CONTROVERSY_BYPASS, true);
				return generateTask(arguments).run_st();
			}
			if (raw_json == null || raw_json.equalsIgnoreCase("null")) {
				if (isEnabled(arguments, MAP_TYPE_HAS_FALLEN_BACK)) {
					return new ExtractedVideo(null, null, ExtractionResult.FAILED).setMessage("FAILED_STAGE_UNABLE_TO_GET_METADATA")
							.setYTMessage("Could not fallback to different extractor due to option.");
				}
				logger.error("YouTubeNextFEEMT -> Extraction failed! Trying embed API!");
				return new YouTubeEmbedNextExtractor().generateTask(arguments).run_st();
			}
			final UniJsonValue parsedJson = UniJsonValue.parse(raw_json, true);
			final UniJsonValue args = parsedJson.contains("args") ? parsedJson.get("args") : parsedJson;
			final UniJsonValue metadataCore = (args.isNull() || !args.contains("player_response") ? args
					: UniJsonValue.parse(args.getString("player_response"), true));
			final UniJsonValue metadataObject = metadataCore.get("videoDetails");
			final variable<Playlist> pl = new variable<Playlist>(null);
			final Executable<YouTubeMetadata> metadata = td.submit(() -> {
				final List<YouTubeCaptions> captions = new LinkedList<YouTubeCaptions>();
				if (metadataCore.contains("captions")) {
					// captions found
					final UniJsonValue ct = metadataCore.getSafe("captions").getSafe("playerCaptionsTracklistRenderer").getIgnoreNull("captionTracks");
					if (ct == null) {
						logger.error("Failed to load captions, captionTracks was null."); // probably no subtitles or contributions are only enabled.
					} else {
						final UniJsonIterableValue captionArray = ct.asArray();
						if (captionArray != null && !captionArray.isNull()) {
							for (final UniJsonValue _v : captionArray) {
								final UniJsonValue caption = _v;
								final String captionURL = caption.getString("baseUrl");
								final String captionText = StringsUtil._capitalizeString(caption.get("name").getString("simpleText"));
								final String captionCode = caption.getString("languageCode");
								captions.add(new YouTubeCaptions(captionURL, captionText, captionCode));
							}
						}
					}
				}
				UniJsonValue keywords = UniJsonValue.array();
				try {
					keywords = metadataObject.get("keywords").asArray();
				} catch (Exception _e) {
				}
				if (keywords == null) {
					logger.warn("Fixed keywords!");
					keywords = UniJsonValue.array();
				}
//		    	final String test_s = StringsUtil.substringBetweenO(result,
//						"window[\"ytInitialData\"] = {", "}}}}]}}},\"contents\":{\"twoColumnWatchNextResults\":{\"results\":{\"results\":{\"contents\":[{");
//				if (test_s != null) {
//					final String test = 
//							"{" + test_s + "}}}}]}}}}";
//					final UniJsonValue obj = UniJsonValue.parse(test).asObject();
//					final UniJsonValue knownEntries = obj.get("responseContext").asObject()
//							.get("webResponseContextExtensionData").asObject().
//							get("webPrefetchData").asObject();
//					pl.set(new Playlist(
//							"N/A"));
//					for (final UniJsonValue _entry : knownEntries.get("navigationEndpoints").asArray()) {
//						final UniJsonValue entry = _entry.asObject();
//						final String title = "[PH] Title NOT extractable.";
//						final String thumbnail = "http://i.ytimg.com/vi/" + 
//								entry.get("watchEndpoint").asObject().getString("videoId")
//								+ "/hqdefault.jpg";
//						final String username = "[PH] Username NOT extractable.";
//						final String link = entry.get("commandMetadata").asObject().get("webCommandMetadata").asObject()
//								.getString("url");
//						final int _index = entry.get("watchEndpoint").asObject().getInt("index", -1);
//						pl.object().addEntry(new PlaylistEntry(title, thumbnail, username, link, _index));
//					}
//				}
				final boolean is360 = args.getString("player_response", "").contains("360°")
						|| args.getString("player_response", "").contains("https://s.youtube.com/api/stats/qoe?cat=spherical");
				final YouTubeMetadata _metadata = new YouTubeMetadata(videoID, metadataObject.getString("title"), metadataObject.getString("author"),
						metadataObject.getString("channelId"), metadataObject.getLong("viewCount", -1), metadataObject.getLong("lengthSeconds", -1),
						metadataObject.getDouble("averageRating", 5), -1, -1, is360, keywords.toArray(), captions);
				_metadata.setVR180(is360 && true); // FIXME: Proper soluton.
				_metadata.limitedExtraction(result.contains("verify_controversy") || arguments.containsKey(MAP_TYPE_ENABLE_CONTROVERSY_BYPASS));
//				try {
//					final long doc_start = System.currentTimeMillis();
//					final Document doc = Jsoup.parse(result);
//					final long doc_end = System.currentTimeMillis();
//					logger.debug("Jsoup parsing time: " + (doc_end - doc_start));
//					metadata.setMap(result);
//					metadata.setDescription(doc.getElementById("eow-description").html());
//				}catch (Exception _e) {
				_metadata.setMap(result);
				_metadata.setDescription(metadataObject.getString("shortDescription", "A description wasn't found.").replaceAll(Pattern.quote("\r\n"), "<br>")
						.replaceAll(Pattern.quote("\n"), "<br>"));
//				}
				return _metadata;
			});
			if (metadataCore.hasValue("streamingData")) {
				final ArrayDeque<Executable<Object>> queue = new ArrayDeque<Executable<Object>>();
				final UniJsonValue streamingData = metadataCore.get("streamingData");
				logger.debug("=> Processing legacy map.");
				if (USE_OLD_FMT_METHOD) {
					final String legacy_map = args.getString("url_encoded_fmt_stream_map");
					final String[] legacyVideos = legacy_map.split(",");
					for (final String legacyVideo : legacyVideos) {
						final Map<String, String> vargs = LegacyURIUtils.queryStringToMap(legacyVideo);
						String vurl = vargs.get("url");
						if (vurl == null) {
							logger.error("vurl == null! Aborting legacy processor.");
							break;
						}
						if (vargs.containsKey("sp")) {
							// encrypted.
							vurl += "?" + vargs.get("sp") + "=" + e.waitTillCompletion().decrypt(vargs.get("s"));
						} else {
							// not needed :)
						}
						final int itag = MathUtil.safeParseInt(vargs.get("itag"), -1);
						final Map<String, String> urlargs = LegacyURIUtils.queryStringToMap(vurl);
						results.add(new YouTubeVideo(vurl, false, itag, Formats.get(itag)).setCodecMime(vargs.get("type"))
								.setFileSize(MathUtil.safeParseL(urlargs.get("clen"), -1)));
						urlargs.clear();
						vargs.clear();
					}
				} else {
					try {
						final UniJsonIterableValue formats = streamingData.get("formats").asArray();
						for (final UniJsonValue _v : formats) {
							final UniJsonValue v = _v;
							final Executable<Object> o = td.makeTask(() -> {
								String vurl = v.getString("url");
								if (v.contains("cipher", "signatureCipher")) {
									final Map<String, String> y = LegacyURIUtils
											.queryStringToMap(v.getStrings("signatureCipher", "cipher").replaceAll(Pattern.quote("\\u0026"), "&"));
									logger.trace("cipher: " + y.get("s"));
									vurl = y.get("url");
									if (vurl == null) {
										return null;
									}
									if (vurl.contains("&dur=0.000")) {
										logger.warn("A source had zero duration, skipped.");
										return null;
									}
									final String x = e.waitTillCompletion().decrypt(y.get("s"));
									logger.trace("deciphered: " + x);
									vurl += "&" + y.get("sp") + "=" + x;
								}
								if (vurl.contains("&dur=0.000")) {
									logger.warn("A source had zero duration, skipped.");
									return null;
								}
								final int itag = v.getInt("itag");
								results.add(new YouTubeVideo(vurl, false, itag, Formats.get(itag)).setCodecMime(v.getString("mimeType"))
										.setFileSize(v.getLong("contentLength", -1)));
								return null;
							});
							td.queueTask(o);
							queue.add(o);
						}
					} catch (Exception ex) {
						ex.printStackTrace();
					}
				}
				logger.debug("=> Processing new map.");
				if (USE_OLD_FMT_METHOD) {
					final String new_map = args.getString("adaptive_fmts");
					final String[] newVideos = new_map.split(",");
					for (final String video : newVideos) {
						final Map<String, String> vargs = LegacyURIUtils.queryStringToMap(video);
						String vurl = vargs.get("url");
						if (vurl == null) {
							logger.error("vurl == null! Aborting new processor.");
							break;
						}
						if (vurl.contains("&dur=0.000")) {
							logger.warn("A source had zero duration, skipped.");
							continue;
						}
						if (vargs.containsKey("sp")) {
							// encrypted.
							vurl += "?" + vargs.get("sp") + "=" + e.waitTillCompletion().decrypt(vargs.get("s"));
						} else {
							// not needed :)
						}
						final int itag = MathUtil.safeParseInt(vargs.get("itag"), -1);
						// urlargs: NOT NEEDED
						results.add(new YouTubeVideo(vurl, true, itag, Formats.auto(itag, vargs, vurl)).setCodecMime(vargs.get("type"))
								.setFileSize(MathUtil.safeParseL(vargs.get("clen"), -1)).setSize(vargs.get("size")).setInitPayload(vargs.get("init"))
								.setIndexPayload(vargs.get("index")).setFPS(MathUtil.safeParseInt(vargs.get("fps"), 24))
								.setBitrate(MathUtil.safeParseInt(vargs.get("bitrate"), 80000)));
						vargs.clear();
					}
				} else {
					final UniJsonIterableValue formats = streamingData.get("adaptiveFormats").asArray();
					for (final UniJsonValue _v : formats) {
						try {
							final UniJsonValue v = _v;
							final Executable<Object> o = td.makeTask(() -> {
								String vurl = v.getString("url", null);
								if (v.contains("cipher", "signatureCipher")) {
									final Map<String, String> y = LegacyURIUtils
											.queryStringToMap(v.contains("signatureCipher") ? v.getString("signatureCipher") : v.getString("cipher"));
									logger.trace("cipher: " + y.get("s"));
									vurl = y.get("url");
									if (vurl == null) {
										return null;
									}
									final String x = e.waitTillCompletion().decrypt(y.get("s"));
									logger.trace("deciphered: " + x);
									vurl += "&" + y.get("sp") + "=" + x;
								}
								if (vurl.contains("&dur=0.000")) {
									logger.warn("A source had zero duration, skipped.");
									return null;
								}
								final int itag = v.getInt("itag");
								results.add(
										new YouTubeVideo(vurl, true, itag, Formats.auto(itag, new UniJsonValue(v), vurl)).setCodecMime(v.getString("mimeType"))
												.setFileSize(v.getLong("contentLength", -1)).setSize(v.getInt("width", 0) + "x" + v.getInt("height", 0))
												.setInitPayload(v.getSafe("initRange").getString("start") + "-" + v.getSafe("initRange").getString("end"))
												.setIndexPayload(v.getSafe("indexRange").getString("start") + "-" + v.getSafe("indexRange").getString("end"))
												.setFPS(v.getInt("fps", -1)).setBitrate(v.getInt("bitrate", 80000)));
								return null;
							});
							td.queueTask(o);
							queue.add(o);
						} catch (Exception ec) {
							logger.error("Failed to process adaptive format!", ec);
						}
					}
				}
				if (queue.isEmpty()) {
				} else {
					while (!queue.isEmpty()) {
						final Executable<Object> _task = queue.poll();
						_task.waitTillCompletion();
					}
				}
			}
			logger.debug("=> Processing metadata.");
			final UniJsonValue player_response = args;
			logger.debug("[metadata] player_response");
			if (metadataObject.hasValue("isUpcoming") && metadataObject.getBoolean("isUpcoming")) {
				logger.error("Failed -> This video isn't live yet!");
				final String liveChatCont = StringsUtil.substringBetweenO(result,
						"\"conversationBar\":{\"liveChatRenderer\":{\"continuations\":[{\"reloadContinuationData\":{\"continuation\":\"",
						"\",\"clickTrackingParams\":\"");
				final long time = player_response.get("playabilityStatus").get("liveStreamability").get("liveStreamabilityRenderer").get("offlineSlate")
						.get("liveStreamOfflineSlateRenderer").getLong("scheduledStartTime", -1);
				final String times = TimeUtil.formatSecondsL(time - System.currentTimeMillis() / 1000);
				return new ExtractedVideo(results, metadata.waitTillCompletion(), ExtractionResult.FAILED).setMessage("ERROR_UPCOMING_STREAM")
						.setYTMessage(
								"This video/stream will be live in " + times + " from now. (Stream Start: " + Dates.time((long) (time * 1000)).toString() + ")")
						.setChatPayload(liveChatCont);
			}
			logger.debug("[metadata] isUpcoming::check");
			if (player_response.hasValue("streamingData")) {
				final String liveChatCont = StringsUtil.substringBetweenO(result,
						"\"conversationBar\":{\"liveChatRenderer\":{\"continuations\":[{\"reloadContinuationData\":{\"continuation\":\"",
						"\",\"clickTrackingParams\":\"");
				final UniJsonValue streamingData = metadataCore.get("streamingData");
				if (streamingData.hasValue("hlsManifestUrl")) {
					logger.debug("Parsing a live video...");
					final String hlsMetadata = streamingData.getString("hlsManifestUrl");
					results.clear();
					results.add(new YouTubeVideo(hlsMetadata, false, 0, new Format(0, FileType.M3U8, 30, 720, VideoCodec.H264, AudioCodec.AAC, -1, false))
							.setLivestream(true).setInternalProxy(true).setFormatType(FormatType.M3U8_STREAM));
					return new ExtractedVideo(results, metadata.waitTillCompletion(), ExtractionResult.SUCCESS).setChatPayload(liveChatCont);
				}
			}
			logger.debug("[metadata] streamingData::check");
			String cxt = null;
			if (ytInitialData.waitTillCompletion() != null) {
				final UniJsonIterableValue o = ytInitialData.result().get("contents").get("twoColumnWatchNextResults").get("results").get("results")
						.get("contents").asArray();
				for (final UniJsonValue v : o) {
					if (v.hasValue("itemSectionRenderer")) {
						final UniJsonValue section = v.get("itemSectionRenderer");
						if (section.getString("sectionIdentifier", "").equalsIgnoreCase("comment-item-section")) {
							try {
								if (section.contains("continuations")) {
									cxt = section.get("continuations").asArray().get(0).get("nextContinuationData").getString("continuation");
								} else {
									cxt = section.get("contents").asArray().get(0).get("continuationItemRenderer").get("continuationEndpoint")
											.get("continuationCommand").getString("token");
								}
								break;
							} catch (Exception ex) {
								logger.error("Failed to get comment tokens!", ex);
							}
						} else {
							continue;
						}
					} else {
						continue;
					}
				}
			}
			if (ytInitialData.waitTillCompletion() != null) {
				try {
					final UniJsonValue o = ytInitialData.result().get("contents").get("twoColumnWatchNextResults");
					if (o.hasValue("playlist")) {
						final UniJsonValue p = o.get("playlist").get("playlist");
						int currentIndex = p.getInt("currentIndex", -1);
						if (currentIndex == -1) {
							logger.error("Failed to track playlist index!");
						} else {
							logger.debug("current playlist index: " + currentIndex);
							pl.set(new Playlist(p.getString("title"), currentIndex));
							for (final UniJsonValue _v : p.get("contents").asArray()) {
								if (_v.hasValue("playlistPanelVideoRenderer")) {
									final UniJsonValue v = _v.get("playlistPanelVideoRenderer");
									final String _videoID = v.getString("videoId");
									final int index = v.get("navigationEndpoint").get("watchEndpoint").getInt("index");
									final String _url = "/watch?v=" + _videoID + "&list="
											+ v.get("navigationEndpoint").get("watchEndpoint").getString("playlistId") + "&index=" + (index + 1);
									String title = "[FIXME] Unknown.";
									if (v.hasValue("title")) {
										if (v.get("title").hasValue("simpleText")) {
											title = v.get("title").getString("simpleText");
										} else {
											logger.warn("Missing simpleText in title object!");
											logger.warn(v.toString());
										}
									}
									String author = "[FIXME] Unknown.";
									if (v.hasValue("longBylineText")) {
										if (v.get("longBylineText").hasValue("runs")) {
											author = v.get("longBylineText").get("runs").get(0).getString("text");
										} else {
											logger.warn("Missing text in longBylineText.runs object!");
											logger.warn(v.toString());
										}
									}
									pl.object()
											.addEntry(new PlaylistEntry(title, "https://i.ytimg.com/vi/" + _videoID + "/mqdefault.jpg", author, _url, index));
								} else {
									continue;
								}
							}
							pl.object().makeCurrent(pl.object().at(currentIndex));
							try {
								pl.object().makeNext(pl.object().at(currentIndex + 1));
							} catch (Exception ex) {
								if (ex instanceof IndexOutOfBoundsException) {
									logger.warn("Failed to set next playlist value, assuming end of playlist.");
								} else {
									logger.error("Failed to get next playlist value!", ex);
								}
							}
							logger.debug("Found playlist!");
						}
					}
				} catch (Exception ec) {
					logger.error("[playlistGen] Failed to get ytInitialData!", ec);
				}
			}
			logger.debug("[ytInitialData] process");
			final YouTubeMetadata md = metadata.waitTillCompletion();
			logger.debug("[finish] md_get");
			return new ExtractedVideo(results, md, ExtractionResult.SUCCESS).setPlaylist(pl.object()).setCommentInitData(new YTCommentInitData(videoID,
					cookiePayload.object(), "1", YouTubeExtractorConstants.CLIENT_VERSION, cxt, SharedExtractorUtil.getXSRFToken(result)));
		});
		return task;
	}

	@Override
	public Object getFlag(final int flag) {
		switch (flag) {
			case FLAG_EXTRACTOR_SUPPORTS_LIVESTREAM_CHAT:
			case FLAG_EXTRACTOR_SUPPORTS_LIVESTREAMS:
				return FLAG_TRUE;
			case FLAG_EXTRACTOR_SUPPORTS_AGE_GATE:
				return FLAG_FALSE;
			case FLAG_EXTRACTOR_SUPPORTS_ABI_2_2:
				return FLAG_TRUE;
			case FLAG_EXTRACTOR_GET_SUPPORTED_ABI:
				return 2.2f;
			default:
				return FLAG_NOT_SUPPORTED;
		}
	}

	@Override
	public String getID() {
		return "YTE.next.nextFEE.MT";
	}

	@Override
	public int abiVersion() {
		return 3510;
	}
}
